# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of logilab-common.
#
# logilab-common is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option) any
# later version.
#
# logilab-common is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with logilab-common.  If not, see <http://www.gnu.org/licenses/>.
import tempfile
import os
from os.path import join, dirname, abspath
import re

from sys import version_info

from six import integer_types

from logilab.common import attrdict
from logilab.common.compat import StringIO
from logilab.common.testlib import TestCase, unittest_main
from logilab.common.optik_ext import OptionValueError
from logilab.common.configuration import Configuration, OptionError, \
     OptionsManagerMixIn, OptionsProviderMixIn, Method, read_old_config, \
     merge_options

DATA = join(dirname(abspath(__file__)), 'data')

OPTIONS = [('dothis', {'type':'yn', 'action': 'store', 'default': True, 'metavar': '<y or n>'}),
           ('value', {'type': 'string', 'metavar': '<string>', 'short': 'v'}),
           ('multiple', {'type': 'csv', 'default': ['yop', 'yep'],
                         'metavar': '<comma separated values>',
                         'help': 'you can also document the option'}),
           ('number', {'type': 'int', 'default':2, 'metavar':'<int>', 'help': 'boom'}),
           ('bytes', {'type': 'bytes', 'default':'1KB', 'metavar':'<bytes>'}),
           ('choice', {'type': 'choice', 'default':'yo', 'choices': ('yo', 'ye'),
                       'metavar':'<yo|ye>'}),
           ('multiple-choice', {'type': 'multiple_choice', 'default':['yo', 'ye'],
                                'choices': ('yo', 'ye', 'yu', 'yi', 'ya'),
                                'metavar':'<yo|ye>'}),
           ('named', {'type':'named', 'default':Method('get_named'),
                      'metavar': '<key=val>'}),

           ('diffgroup', {'type':'string', 'default':'pouet', 'metavar': '<key=val>',
                          'group': 'agroup'}),
           ('reset-value', {'type': 'string', 'metavar': '<string>', 'short': 'r',
                            'dest':'value'}),

           ('opt-b-1', {'type': 'string', 'metavar': '<string>', 'group': 'bgroup'}),
           ('opt-b-2', {'type': 'string', 'metavar': '<string>', 'group': 'bgroup'}),
           ]

class MyConfiguration(Configuration):
    """test configuration"""
    def get_named(self):
        return {'key': 'val'}

class ConfigurationTC(TestCase):

    def setUp(self):
        self.cfg = MyConfiguration(name='test', options=OPTIONS, usage='Just do it ! (tm)')

    def test_default(self):
        cfg = self.cfg
        self.assertEqual(cfg['dothis'], True)
        self.assertEqual(cfg['value'], None)
        self.assertEqual(cfg['multiple'], ['yop', 'yep'])
        self.assertEqual(cfg['number'], 2)
        self.assertEqual(cfg['bytes'], 1024)
        self.assertIsInstance(cfg['bytes'], integer_types)
        self.assertEqual(cfg['choice'], 'yo')
        self.assertEqual(cfg['multiple-choice'], ['yo', 'ye'])
        self.assertEqual(cfg['named'], {'key': 'val'})

    def test_base(self):
        cfg = self.cfg
        cfg.set_option('number', '0')
        self.assertEqual(cfg['number'], 0)
        self.assertRaises(OptionValueError, cfg.set_option, 'number', 'youpi')
        self.assertRaises(OptionValueError, cfg.set_option, 'choice', 'youpi')
        self.assertRaises(OptionValueError, cfg.set_option, 'multiple-choice', ('yo', 'y', 'ya'))
        cfg.set_option('multiple-choice', 'yo, ya')
        self.assertEqual(cfg['multiple-choice'], ['yo', 'ya'])
        self.assertEqual(cfg.get('multiple-choice'), ['yo', 'ya'])
        self.assertEqual(cfg.get('whatever'), None)

    def test_load_command_line_configuration(self):
        cfg = self.cfg
        args = cfg.load_command_line_configuration(['--choice', 'ye', '--number', '4',
                                                    '--multiple=1,2,3', '--dothis=n',
                                                    '--bytes=10KB',
                                                    'other', 'arguments'])
        self.assertEqual(args, ['other', 'arguments'])
        self.assertEqual(cfg['dothis'], False)
        self.assertEqual(cfg['multiple'], ['1', '2', '3'])
        self.assertEqual(cfg['number'], 4)
        self.assertEqual(cfg['bytes'], 10240)
        self.assertEqual(cfg['choice'], 'ye')
        self.assertEqual(cfg['value'], None)
        args = cfg.load_command_line_configuration(['-v', 'duh'])
        self.assertEqual(args, [])
        self.assertEqual(cfg['value'], 'duh')
        self.assertEqual(cfg['dothis'], False)
        self.assertEqual(cfg['multiple'], ['1', '2', '3'])
        self.assertEqual(cfg['number'], 4)
        self.assertEqual(cfg['bytes'], 10240)
        self.assertEqual(cfg['choice'], 'ye')

    def test_load_configuration(self):
        cfg = self.cfg
        args = cfg.load_configuration(choice='ye', number='4',
                                      multiple='1,2,3', dothis='n',
                                      multiple_choice=('yo', 'ya'))
        self.assertEqual(cfg['dothis'], False)
        self.assertEqual(cfg['multiple'], ['1', '2', '3'])
        self.assertEqual(cfg['number'], 4)
        self.assertEqual(cfg['choice'], 'ye')
        self.assertEqual(cfg['value'], None)
        self.assertEqual(cfg['multiple-choice'], ('yo', 'ya'))

    def test_load_configuration_file_case_insensitive(self):
        file = tempfile.mktemp()
        stream = open(file, 'w')
        try:
            stream.write("""[Test]

dothis=no

#value=

# you can also document the option
multiple=yop,yepii

# boom
number=3

bytes=1KB

choice=yo

multiple-choice=yo,ye

named=key:val


[agroup]

diffgroup=zou
""")
            stream.close()
            self.cfg.load_file_configuration(file)
            self.assertEqual(self.cfg['dothis'], False)
            self.assertEqual(self.cfg['value'], None)
            self.assertEqual(self.cfg['multiple'], ['yop', 'yepii'])
            self.assertEqual(self.cfg['diffgroup'], 'zou')
        finally:
            os.remove(file)

    def test_option_order(self):
        """ Check that options are taken into account in the command line order
            and not in the order they are defined in the Configuration object.
        """
        file = tempfile.mktemp()
        stream = open(file, 'w')
        try:
            stream.write("""[Test]
reset-value=toto
value=tata
""")
            stream.close()
            self.cfg.load_file_configuration(file)
        finally:
            os.remove(file)
        self.assertEqual(self.cfg['value'], 'tata')

    def test_unsupported_options(self):
        file = tempfile.mktemp()
        stream = open(file, 'w')
        try:
            stream.write("""[Test]
whatever=toto
value=tata
""")
            stream.close()
            self.cfg.load_file_configuration(file)
        finally:
            os.remove(file)
        self.assertEqual(self.cfg['value'], 'tata')
        self.assertRaises(OptionError, self.cfg.__getitem__, 'whatever')

    def test_generate_config(self):
        stream = StringIO()
        self.cfg.generate_config(stream)
        self.assertMultiLineEqual(stream.getvalue().strip(), """[TEST]

dothis=yes

#value=

# you can also document the option
multiple=yop,yep

# boom
number=2

bytes=1KB

choice=yo

multiple-choice=yo,ye

named=key:val

#reset-value=


[AGROUP]

diffgroup=pouet


[BGROUP]

#opt-b-1=

#opt-b-2=""")

    def test_generate_config_with_space_string(self):
        self.cfg['value'] = '    '
        stream = StringIO()
        self.cfg.generate_config(stream)
        self.assertMultiLineEqual(stream.getvalue().strip(), """[TEST]

dothis=yes

value='    '

# you can also document the option
multiple=yop,yep

# boom
number=2

bytes=1KB

choice=yo

multiple-choice=yo,ye

named=key:val

reset-value='    '


[AGROUP]

diffgroup=pouet


[BGROUP]

#opt-b-1=

#opt-b-2=""")

    def test_generate_config_with_multiline_string(self):
        self.cfg['value'] = 'line1\nline2\nline3'
        stream = StringIO()
        self.cfg.generate_config(stream)
        self.assertMultiLineEqual(stream.getvalue().strip(), """[TEST]

dothis=yes

value=
    line1
    line2
    line3

# you can also document the option
multiple=yop,yep

# boom
number=2

bytes=1KB

choice=yo

multiple-choice=yo,ye

named=key:val

reset-value=
    line1
    line2
    line3


[AGROUP]

diffgroup=pouet


[BGROUP]

#opt-b-1=

#opt-b-2=""")


    def test_roundtrip(self):
        cfg = self.cfg
        f = tempfile.mktemp()
        stream = open(f, 'w')
        try:
            self.cfg['dothis'] = False
            self.cfg['multiple'] = ["toto", "tata"]
            self.cfg['number'] = 3
            self.cfg['bytes'] = 2048
            cfg.generate_config(stream)
            stream.close()
            new_cfg = MyConfiguration(name='test', options=OPTIONS)
            new_cfg.load_file_configuration(f)
            self.assertEqual(cfg['dothis'], new_cfg['dothis'])
            self.assertEqual(cfg['multiple'], new_cfg['multiple'])
            self.assertEqual(cfg['number'], new_cfg['number'])
            self.assertEqual(cfg['bytes'], new_cfg['bytes'])
            self.assertEqual(cfg['choice'], new_cfg['choice'])
            self.assertEqual(cfg['value'], new_cfg['value'])
            self.assertEqual(cfg['multiple-choice'], new_cfg['multiple-choice'])
        finally:
            os.remove(f)

    def test_setitem(self):
        self.assertRaises(OptionValueError,
                          self.cfg.__setitem__, 'multiple-choice', ('a', 'b'))
        self.cfg['multiple-choice'] = ('yi', 'ya')
        self.assertEqual(self.cfg['multiple-choice'], ('yi', 'ya'))

    def test_help(self):
        self.cfg.add_help_section('bonus', 'a nice additional help')
        help = self.cfg.help().strip()
        # at least in python 2.4.2 the output is:
        # '  -v <string>, --value=<string>'
        # it is not unlikely some optik/optparse versions do print -v<string>
        # so accept both
        help = help.replace(' -v <string>, ', ' -v<string>, ')
        help = re.sub('[ ]*(\r?\n)', '\\1', help)
        USAGE = """Usage: Just do it ! (tm)

Options:
  -h, --help            show this help message and exit
  --dothis=<y or n>
  -v<string>, --value=<string>
  --multiple=<comma separated values>
                        you can also document the option [current: yop,yep]
  --number=<int>        boom [current: 2]
  --bytes=<bytes>
  --choice=<yo|ye>
  --multiple-choice=<yo|ye>
  --named=<key=val>
  -r <string>, --reset-value=<string>

  Agroup:
    --diffgroup=<key=val>

  Bgroup:
    --opt-b-1=<string>
    --opt-b-2=<string>

  Bonus:
    a nice additional help"""
        if version_info < (2, 5):
            # 'usage' header is not capitalized in this version
            USAGE = USAGE.replace('Usage: ', 'usage: ')
        elif version_info < (2, 4):
            USAGE = """usage: Just do it ! (tm)

options:
  -h, --help            show this help message and exit
  --dothis=<y or n>
  -v<string>, --value=<string>
  --multiple=<comma separated values>
                        you can also document the option
  --number=<int>
  --choice=<yo|ye>
  --multiple-choice=<yo|ye>
  --named=<key=val>

  Bonus:
    a nice additional help
"""
        self.assertMultiLineEqual(help, USAGE)


    def test_manpage(self):
        pkginfo = {}
        with open(join(DATA, '__pkginfo__.py')) as fobj:
            exec(fobj.read(), pkginfo)
        self.cfg.generate_manpage(attrdict(pkginfo), stream=StringIO())

    def test_rewrite_config(self):
        changes = [('renamed', 'renamed', 'choice'),
                   ('moved', 'named', 'old', 'test'),
                   ]
        read_old_config(self.cfg, changes, join(DATA, 'test.ini'))
        stream = StringIO()
        self.cfg.generate_config(stream)
        self.assertMultiLineEqual(stream.getvalue().strip(), """[TEST]

dothis=yes

value='    '

# you can also document the option
multiple=yop

# boom
number=2

bytes=1KB

choice=yo

multiple-choice=yo,ye

named=key:val

reset-value='    '


[AGROUP]

diffgroup=pouet


[BGROUP]

#opt-b-1=

#opt-b-2=""")

class Linter(OptionsManagerMixIn, OptionsProviderMixIn):
    options = (
        ('profile', {'type' : 'yn', 'metavar' : '<y_or_n>',
                     'default': False,
                     'help' : 'Profiled execution.'}),
        )
    def __init__(self):
        OptionsManagerMixIn.__init__(self, usage="")
        OptionsProviderMixIn.__init__(self)
        self.register_options_provider(self)
        self.load_provider_defaults()

class RegrTC(TestCase):

    def setUp(self):
        self.linter = Linter()

    def test_load_defaults(self):
        self.linter.load_command_line_configuration([])
        self.assertEqual(self.linter.config.profile, False)

    def test_register_options_multiple_groups(self):
        """ensure multiple option groups can be registered at once"""
        config = Configuration()
        self.assertEqual(config.options, ())
        new_options = (
            ('option1', {'type': 'string', 'help': '',
                         'group': 'g1', 'level': 2}),
            ('option2', {'type': 'string', 'help': '',
                         'group': 'g1', 'level': 2}),
            ('option3', {'type': 'string', 'help': '',
                         'group': 'g2', 'level': 2}),
        )
        config.register_options(new_options)
        self.assertEqual(config.options, new_options)


class MergeTC(TestCase):

    def test_merge1(self):
        merged = merge_options([('dothis', {'type':'yn', 'action': 'store', 'default': True,  'metavar': '<y or n>'}),
                                ('dothis', {'type':'yn', 'action': 'store', 'default': False, 'metavar': '<y or n>'}),
                                ])
        self.assertEqual(len(merged), 1)
        self.assertEqual(merged[0][0], 'dothis')
        self.assertEqual(merged[0][1]['default'], True)

    def test_merge2(self):
        merged = merge_options([('dothis', {'type':'yn', 'action': 'store', 'default': True,  'metavar': '<y or n>'}),
                                ('value', {'type': 'string', 'metavar': '<string>', 'short': 'v'}),
                                ('dothis', {'type':'yn', 'action': 'store', 'default': False, 'metavar': '<y or n>'}),
                                ])
        self.assertEqual(len(merged), 2)
        self.assertEqual(merged[0][0], 'value')
        self.assertEqual(merged[1][0], 'dothis')
        self.assertEqual(merged[1][1]['default'], True)

if __name__ == '__main__':
    unittest_main()
